feat: MCP server for waking and driving IDE / agent tmux sessions#10
feat: MCP server for waking and driving IDE / agent tmux sessions#10ThinkOffApp merged 11 commits intomainfrom
Conversation
Adds a Model Context Protocol (MCP) server that exposes IAK's
tmux-backed wake / list / run primitives as MCP tools, so any
MCP-aware client (Claude Desktop / Code, Cursor, custom agents) can
drive the agent fleet directly without re-implementing the
nudge / send-keys protocol.
New files:
* src/mcp-server.mjs - MCP server (stdio transport).
4 tools: wake_ide, list_sessions, wake_all,
tmux_run. wake_all is config-aware: pulls
sessions from tmux.ide_session +
tmux.default_session + any per-adapter
session keys.
* bin/iak-mcp.mjs - CLI entry point (chmod +x, registered as
the `ide-agent-kit-mcp` bin in package.json
and as the `npm run mcp` script).
Dependencies:
* @modelcontextprotocol/sdk@^1.29.0
Verified by JSON-RPC handshake on stdio: initialize succeeds,
tools/list returns the 4 tools, tools/call name=list_sessions
returns the live tmux sessions on this host (6 found in smoke test).
One small implementation note worth flagging:
the tmux list-sessions format uses '|' as the field delimiter
rather than \t. Single-quoted shell strings do not interpret \t,
so tmux would receive literal backslash-t and emit it verbatim
instead of a tab, which broke the .split('\t') parser in early
iteration. '|' is safe for tmux session names (which are
alphanumeric + dash + underscore in IAK's conventions).
Wiring example in README. Drop in your MCP client config:
{
"mcpServers": {
"ide-agent-kit": {
"command": "node",
"args": ["/absolute/path/to/ide-agent-kit/bin/iak-mcp.mjs"]
}
}
}
…restart the client, the 4 tools appear in the picker, and you can
wake any IDE in the fleet from any agent that speaks MCP.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
To use Codex here, create an environment for this repo. |
ThinkOffApp
left a comment
There was a problem hiding this comment.
Reviewed src/mcp-server.mjs and the new bin/iak-mcp.mjs. Reuses nudgeTmux + tmuxRun cleanly, stdio transport via official @modelcontextprotocol/sdk 1.29.0, schemas + descriptions are well-shaped, config-load failure degrades gracefully. Approve in spirit, with a few items worth addressing before merge:
Security (high): src/ide/tmux-runner.mjs:21 opens with if (!allowlist || allowlist.length === 0) return true; — if config.tmux.allow is missing or empty, tmux_run becomes arbitrary shell execution. Fine for the CLI where the operator chose to invoke it, but the MCP surface can be driven by any client over stdio. Suggest the MCP server fail closed when config.tmux.allow is empty (refuse to register tmux_run and log a warning), or require an explicit mcp.allow_unrestricted=true opt-in.
configuredAgentSessions is fragile (medium): scans every top-level config key for objects with a .session string. Will pick up unrelated future keys (e.g. sentry: { session: "warn" }). Cleaner to add an explicit mcp.sessions: ["claudemb", "antigravity", ...] config key.
Hardcoded version (low): server name advertises version: "0.6.1". Will drift from package.json on future bumps. Read it from ../package.json once at boot.
Missing read tool (medium, scope): agents calling wake_ide have no way to peek at the session output before/after. A read_session(session, lines=50) tool that runs tmux capture-pane -p -t <session> -S -lines would make the toolset much more useful — without it, MCP clients have to wake-and-pray.
No tests: the diff is +1423 / -4 with no test/ delta. The tmux_run wrapper especially deserves a unit test asserting the fail-closed behavior once it lands.
Otherwise solid. Reusing existing allowlist + nudge primitives instead of forking them was the right call.
…ession, tests, version-from-package Addresses every item in @claudemm's PR #10 review: * **Security (high) — tmux_run fail-closed.** When `tmux.allow` is missing or empty AND `mcp.allow_unrestricted` is not true, `tmux_run` is omitted from the tool list entirely (so it cannot be invoked at all). The boot log explains the decision: "[iak-mcp] tmux_run: enabled — tmux.allow has 5 pattern(s)" or "[iak-mcp] tmux_run: disabled — tmux.allow is missing or empty …". New helper decideTmuxRunMode() is exported and unit-tested. * **Fragile session discovery (medium) — explicit mcp.sessions.** Replaced the "scan every top-level config key for objects with a .session string" heuristic with an explicit `mcp.sessions: [...]` config key (preferred) plus the existing tmux.ide_session + tmux.default_session fallback. Unrelated keys (`sentry: {session: "warn"}`) no longer leak into wake_all targets. * **Hardcoded version (low).** Server now reads its own version from ../package.json at module load instead of hard-coding "0.6.1". Tracks future bumps automatically. * **Missing read_session tool (medium scope).** New tool wraps `tmux capture-pane -p -t <session> -S -<lines>` so MCP clients can see what an agent printed after a wake_ide. Lines clamped to [1, 2000]. * **No tests (medium).** Added test/mcp-server.test.mjs covering: - decideTmuxRunMode: missing config, empty allow, populated allow, allow_unrestricted with empty allow, allow_unrestricted=false. - configuredAgentSessions: empty config, explicit mcp.sessions, fallback to tmux.* keys, dedup, anti-fragility (does NOT scan unrelated keys), drops empty/non-string entries. - End-to-end stdio: server boots, advertises name + real semver version, exposes safe tools; with empty allowlist tmux_run is OMITTED; with mcp.allow_unrestricted=true tmux_run is INCLUDED. All 14 tests pass locally. * **Config pass-through.** loadConfig() now passes `mcp` through as `raw.mcp || {}` (same shape as `openclaw` already does), so MCP server-specific config keys round-trip without being dropped. README updated with the new tool table row, the new fail-closed behavior, and an explicit "MCP-specific config keys" subsection documenting `mcp.sessions` and `mcp.allow_unrestricted`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements the Codewatch / GroupMind confirmation flow @Petrus asked for, on top of the MCP server already in this branch. Architecture: agent --MCP--> request_confirmation(prompt) | \ | \--> GroupMind room: posts the prompt with | /approve <id> · /deny <id> quick replies | (uses poller.api_key + mcp.confirmations.room) | \--> CLAWWATCH_GATE: posts the prompt to CodexMB's PR #8 receiver, which renders an Android interactive notification with Approve / Deny buttons that vibrate the watch. user taps Approve on watch (or types /approve <id> in chat) | v Codewatch's notification action POSTs to http://<callback_base>/intent/<id>/decision {decision: "approve"} | v iak-mcp's tiny HTTP server settles the intent, the in-memory promise resolves, request_confirmation returns synchronously. New files: * src/confirmations.mjs - in-memory intent registry (pending / decided) - createIntent + waitForDecision (synchronous-friendly) - decideIntent (idempotent for same decision; rejects flip-flops) - listIntents - startConfirmationsServer (POST /intent/:id/decision, GET /intents) with optional bearer auth (constant-time check) - announcers: makeGroupmindAnnouncer (HTTPS POST to groupmind.one/api/v1/messages), makeCodewatchAnnouncer (POST to the CLAWWATCH_GATE proxy) - composeAnnouncers (channel fan-out, per-channel error isolation) * test/confirmations.test.mjs — 15 tests: - createIntent: announces, returns id - decideIntent: validates, idempotent, rejects flip-flops - waitForDecision: immediate / blocking / timeout paths - listIntents: shows transitions - createIntent: tolerates announce failures - composeAnnouncers: fan-out + per-channel error isolation - HTTP: end-to-end POST settles a waiter; rejects bad json / unknown id; GET /intents lists; bearer auth gate works mcp-server.mjs additions: * Imports the confirmations module and conditionally starts the HTTP server + wires announcers based on mcp.confirmations config. * Registers four new tools — request_confirmation, list_intents, approve_intent, deny_intent — only if at least one channel is configured. Without a channel they're omitted (fail-closed, same pattern as tmux_run). * Boot log explains which channels are armed: "[iak-mcp] confirmations: enabled on http://127.0.0.1:8788 — channels: groupmind, codewatch" or "disabled — set mcp.confirmations.room (+ poller.api_key) and/or mcp.confirmations.codewatch_gate_url". README: * New "Confirmation flow" subsection with config keys + the four tools + an end-to-end walkthrough mapping the watch tap back to the MCP tool's return. Test suite: 76 / 76 pass (61 existing + 15 new). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…aemon * src/confirmations.mjs: GET / and /intents.html serve a small mobile-first HTML page with Approve / Deny tap targets per pending intent (inlined CSS/JS, no external assets, auto-refresh 2s). GroupMind announcer message body now includes a 'Tap to decide:' link to the same UI URL. * bin/iak-mcp-daemon.mjs (new): long-running daemon — starts the HTTP listener and a chat-reply poller that watches the configured GroupMind room every 5s for '/approve <id>' / '/deny <id>' and routes to the local intent endpoint. --demo flag posts a verification intent at boot. End-to-end verified live: tap the link in chat OR /approve <id> reply both land at /intent/<id>/decision and settle a waiting request_confirmation call. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
makeGroupmindAnnouncer now includes:
metadata: { actions: ["Approve","Deny"], intent_id,
intent_prompt, intent_session }
Companion change in antfarm (PR https://github.com/ThinkOffApp/antfarm/pull) renders inline Approve/Deny buttons on chat messages whose metadata carries both fields. Tap → posts `/approve <id>` as a chat reply → existing chat-reply poller in this daemon catches it and routes to /intent/:id/decision.
End-to-end: agent calls request_confirmation MCP tool → daemon posts to room with metadata → user taps Approve in GroupMind chat → chat reply posted → daemon catches reply → intent settled → MCP tool resolves with {decision: "approve"}.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the missing piece for unattended Claude Code:
- scripts/claudemb-poll.sh synced with the production version
on the MacBook (DM polling, focus-
preserving wake, cooldown, dual
/tmp file paths). Same script that
has been driving @claudemb's
auto-respond loop in the
thinkoff-development room.
- scripts/claudemb-wake.sh AppleScript injector. Activates
the Claude desktop app, types the
nudge ("check rooms"), then restores
focus to whatever app was frontmost.
- scripts/check-rooms-hook.sh UserPromptSubmit hook. Reads
/tmp/iak-new-messages.txt and
prepends to the prompt, then clears.
- docs/auto-wake.md Full setup + ASCII diagram +
env-var reference + troubleshooting.
Lets @claudemm and any other Claude desktop instance mirror the
auto-wake loop on a fresh box: drop the three scripts, wire the hook,
start the poller in tmux, done.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
scripts/claudecode-stop-resume.sh — Claude Code Stop hook that reads /tmp/iak-new-messages.txt and exits 2 with content on stderr, which makes Claude Code resume the turn with the new messages as additional context. No Accessibility permission needed. Pairs with the existing osascript wake path: the AppleScript wake covers from-idle (creates a new turn from nothing); the Stop hook covers in-flight (catches messages that arrive during an active turn). Use either or both depending on your macOS Accessibility state. Idea + reference impl from @claudemm on the Mac mini. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- src/confirmations.mjs: startConfirmationsServer now accepts an
optional `announce` callback. New POST /intent endpoint creates a
pending intent, wires up announcements via that callback, and
returns {ok, id}. Lets any HTTP caller (Bash gates, external
scripts, other agents) create confirmations without going through
stdio MCP.
- bin/iak-mcp-daemon.mjs: builds the announcer map up-front and
passes it into startConfirmationsServer so POST /intent fires
GroupMind/Codewatch announcements out of the box.
- src/mcp-server.mjs: request_confirmation now probes for a live
daemon at startup; when present, forwards intent creation +
decision polling via HTTP instead of starting an in-process
HTTP listener (which would conflict with the daemon's port).
Result: many MCP clients can share one daemon's intent registry.
- test/real-agent-demo.mjs: tiny MCP-SDK client harness that
spawns iak-mcp-server and calls request_confirmation, useful
for end-to-end testing.
Unblocks @claudemm's unification plan: a PreToolUse Bash gate on the
mini can now POST /intent + poll for the decision instead of going
through the older watch-gate.py /relay/events path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- src/confirmations.mjs: startConfirmationsServer accepts an optional
wakeScript path. New POST /wake endpoint runs the script with
the requested text (default "check rooms") and returns 202. Spawns
detached so the response returns immediately.
- bin/iak-mcp-daemon.mjs: defaults wakeScript to scripts/claudemb-wake.sh
in the repo. Override via mcp.confirmations.wake_script.
- src/mcp-server.mjs: new wake_remote MCP tool. Body {gateUrl, text}.
Forwards to the remote daemon's POST /wake. Lets any agent with the
IAK MCP registered nudge another agent's desktop app within ~500ms,
bypassing the 15s room-poll cadence.
Use case: claudemm posts a confirmation that needs claudemb's input.
Their daemon calls wake_remote(gateUrl="http://192.168.50.240:8788")
and claudemb's desktop Claude gets a "check rooms" prompt instantly
instead of waiting up to 15s for the next IAK poller tick.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
curl -fsSL https://raw.githubusercontent.com/ThinkOffApp/ide-agent-kit/main/scripts/install.sh | bash Idempotent. Verifies prereqs (node/git/tmux via brew), clones to ~/ide-agent-kit, npm installs, writes a starter config (with required-edit fields highlighted), wires UserPromptSubmit + Stop hooks into ~/.claude/settings.json, starts the daemon in a tmux session, prints the LAN URL the user pastes into CodeWatch. Does NOT generate keys, touch Accessibility (prints System Settings deep-link), or install Claude Code itself. Pairs with the in-app CodeWatch setup card (separate commit) so a user can go from "fresh phone + fresh Mac" to fully working in about 3 minutes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Adds an MCP server (
src/mcp-server.mjs+bin/iak-mcp.mjs) that exposes IAK's tmux-backed wake / list / run primitives as MCP tools. Any MCP-aware client (Claude Desktop / Code, Cursor, custom agents) can now drive the agent fleet directly without re-implementing the nudge / send-keys protocol.Per @Petrus's room request "use MCP to wake up IDEs".
Tools exposed
wake_idesession,text?(default\"check rooms\")list_sessionswake_alltext?(default\"check rooms\")tmux_runcmd,session?,cwd?,timeoutSec?tmux run.Wiring example (Claude Desktop / Code)
{ \"mcpServers\": { \"ide-agent-kit\": { \"command\": \"node\", \"args\": [\"/absolute/path/to/ide-agent-kit/bin/iak-mcp.mjs\"] } } }Restart the client → the 4 tools appear in the tool picker.
Smoke test (already done)
Files
src/mcp-server.mjs(new) — server, all 4 tool handlersbin/iak-mcp.mjs(new) — stdio CLI entry point, registered as theide-agent-kit-mcpbinpackage.json— adds the bin entry, thenpm run mcpscript, themcpkeyword, and@modelcontextprotocol/sdk@^1.29.0package-lock.json— lockedREADME.md— new "MCP server" subsection under IntegrationsImplementation notes
wake_allis config-aware: pulls sessions fromtmux.ide_session,tmux.default_session, and any per-adaptersessionkeys. Sessions that aren't live are reported withnot runningrather than failed silently.list_sessionsuses|as the tmux format delimiter rather than\\t— single-quoted shell strings don't interpret\\t, so tmux would emit literal backslash-t.|is safe for IAK's session naming.isError: truewith a text message rather than crashing the server.tmux_runreuses the existingtmuxRunfromsrc/ide/tmux-runner.mjsso the allowlist + receipt behavior is identical to the CLI.Test plan
node bin/iak-mcp.mjsboots and emits[iak-mcp] ready on stdio.initialize→ server returns protocolVersion + serverInfo.tools/list→ returns all 4 tools with correct schemas.tools/call name=list_sessions→ returns live tmux sessions on the host.wake_ideactually nudges a target tmux session end-to-end.wake_allwith the live IAK config and verify per-session pass/fail.Out of scope
mcpas a subcommand ofbin/cli.mjs(kept separate via dedicated bin so MCP clients can spawn it cleanly without parsing CLI args).🤖 Generated with Claude Code